import {
  NullableKeys,
  EmptyTypes,
  ExtractParamsType,
  ExtractPayloadType,
  ExtractAdapterType,
  ExtractEndpointType,
  ExtractHasPayloadType,
  ExtractHasParamsType,
  ExtractHasQueryParamsType,
  ExtractErrorType,
  ExtractResponseType,
  HttpMethodsType,
  ExtractQueryParamsType,
  ExtractAdapterOptionsType,
  ExtractAdapterMethodType,
  TypeWithDefaults,
} from "types";
import { Request } from "request";
import { RequestResponseType, ResponseSuccessType, ResponseErrorType } from "adapter";
import { RequestEventType, RequestProgressEventType, RequestResponseEventType } from "managers";
import { ClientInstance } from "client";

// Instance

export type RequestInstanceProperties = {
  response?: any;
  payload?: any;
  error?: any;
  client?: ClientInstance;
  queryParams?: any;
  endpoint?: string;
  hasParams?: boolean;
  hasQueryParams?: boolean;
  hasPayload?: boolean;
};

export type RequestInstance<
  RequestProperties extends RequestInstanceProperties = {
    response?: any;
    payload?: any;
    queryParams?: any;
    error?: any;
    client?: ClientInstance;
  },
> = Request<
  TypeWithDefaults<RequestProperties, "response", any>,
  TypeWithDefaults<RequestProperties, "payload", any>,
  TypeWithDefaults<RequestProperties, "queryParams", any>,
  TypeWithDefaults<RequestProperties, "error", any>,
  TypeWithDefaults<RequestProperties, "endpoint", any>,
  TypeWithDefaults<RequestProperties, "client", ClientInstance>,
  TypeWithDefaults<RequestProperties, "hasParams", any>,
  TypeWithDefaults<RequestProperties, "hasQueryParams", any>,
  TypeWithDefaults<RequestProperties, "hasPayload", any>
>;

// Progress
export type ProgressEventType = { total: number; loaded: number };

/**
 * Dump of the request used to later recreate it
 */
export type RequestJSON<Request extends RequestInstance> = {
  requestOptions: RequestOptionsType<
    ExtractEndpointType<Request>,
    ExtractAdapterOptionsType<ExtractAdapterType<Request>>,
    ExtractAdapterMethodType<ExtractAdapterType<Request>>
  >;
  endpoint: ExtractEndpointType<Request>;
  method: ExtractAdapterMethodType<ExtractAdapterType<Request>>;
  headers?: HeadersInit;
  auth: boolean;
  cancelable: boolean;
  retry: number;
  retryTime: number;
  cacheTime: number;
  cache: boolean;
  staleTime: number;
  queued: boolean;
  offline: boolean;
  disableResponseInterceptors: boolean | undefined;
  disableRequestInterceptors: boolean | undefined;
  options?: ExtractAdapterOptionsType<ExtractAdapterType<Request>>;
  payload: PayloadType<ExtractPayloadType<Request>>;
  params: ExtractParamsType<Request> | EmptyTypes;
  queryParams: ExtractQueryParamsType<Request> | EmptyTypes;
  abortKey: string;
  cacheKey: string;
  queryKey: string;
  used: boolean;
  updatedAbortKey: boolean;
  updatedCacheKey: boolean;
  updatedQueryKey: boolean;
  deduplicate: boolean;
  deduplicateTime: number | null;
  isMockerEnabled: boolean;
  hasMock: boolean;
};

// Request

/**
 * Configuration options for request creation
 */
export type RequestOptionsType<GenericEndpoint, AdapterOptions, RequestMethods = HttpMethodsType> = {
  /**
   * Determine the endpoint for request request
   */
  endpoint: GenericEndpoint;
  /**
   * Custom headers for request
   */
  headers?: HeadersInit;
  /**
   * Should the onAuth method get called on this request
   */
  auth?: boolean;
  /**
   * Request method picked from method names handled by adapter
   * With default adapter it is GET | POST | PATCH | PUT | DELETE
   */
  method?: RequestMethods;
  /**
   * Should enable cancelable mode in the Dispatcher
   */
  cancelable?: boolean;
  /**
   * Retry count when request is failed
   */
  retry?: number;
  /**
   * Retry time delay between retries
   */
  retryTime?: number;
  /**
   * Should we trigger garbage collection or leave data in memory
   */
  cacheTime?: number;
  /**
   * Should we save the response to cache
   */
  cache?: boolean;
  /**
   * Time for which the cache is considered up-to-date
   */
  staleTime?: number;
  /**
   * Should the requests from this request be send one-by-one
   */
  queued?: boolean;
  /**
   * Do we want to store request made in offline mode for latter use when we go back online
   */
  offline?: boolean;
  /**
   * Disable post-request interceptors
   */
  disableResponseInterceptors?: boolean;
  /**
   * Disable pre-request interceptors
   */
  disableRequestInterceptors?: boolean;
  /**
   * Additional options for your adapter, by default XHR options
   */
  options?: AdapterOptions;
  /**
   * Key which will be used to cancel requests. Autogenerated by default.
   */
  abortKey?: string;
  /**
   * Key which will be used to cache requests. Autogenerated by default.
   */
  cacheKey?: string;
  /**
   * Key which will be used to queue requests. Autogenerated by default.
   */
  queryKey?: string;
  /**
   * Should we deduplicate two requests made at the same time into one
   */
  deduplicate?: boolean;
  /**
   * Time of pooling for the deduplication to be active (default 10ms)
   */
  deduplicateTime?: number;
};

export type PayloadMapperType<Payload> = <NewDataType>(payload: Payload) => NewDataType;

export type PayloadType<Payload> = Payload | EmptyTypes;

export type RequestConfigurationType<
  Payload,
  Params,
  QueryParams,
  GenericEndpoint extends string,
  AdapterOptions,
  MethodsType,
> = {
  used?: boolean;
  params?: Params | EmptyTypes;
  queryParams?: QueryParams | EmptyTypes;
  payload?: PayloadType<Payload>;
  headers?: HeadersInit;
  updatedAbortKey?: boolean;
  updatedCacheKey?: boolean;
  updatedQueryKey?: boolean;
  updatedEffectKey?: boolean;
} & Partial<NullableKeys<RequestOptionsType<GenericEndpoint, AdapterOptions, MethodsType>>>;

export type ParamType = string | number;
export type ParamsType = Record<string, ParamType>;

export type ExtractUrlParams<T extends string> = string extends T
  ? EmptyTypes
  : // eslint-disable-next-line @typescript-eslint/no-unused-vars
    T extends `${string}:${infer Param}/${infer Rest}`
    ? { [k in Param | keyof ExtractUrlParams<Rest>]: ParamType }
    : // eslint-disable-next-line @typescript-eslint/no-unused-vars
      T extends `${string}:${infer Param}`
      ? { [k in Param]: ParamType }
      : EmptyTypes;

/**
 * If the request endpoint parameters are not filled it will throw an error
 */
export type FetchParamsType<Params, HasParams extends true | false> = Params extends EmptyTypes | void | never
  ? { params?: EmptyTypes }
  : HasParams extends true
    ? { params?: EmptyTypes }
    : { params: Params };

/**
 * If the request data is not filled it will throw an error
 */
export type FetchPayloadType<Payload, HasPayload extends true | false> = Payload extends EmptyTypes | void | never
  ? { payload?: EmptyTypes }
  : HasPayload extends true
    ? { payload?: EmptyTypes }
    : { payload: Payload };

/**
 * It will check if the query params are already set
 */
export type FetchQueryParamsType<QueryParams, HasQuery extends true | false = false> = HasQuery extends true
  ? { queryParams?: EmptyTypes | undefined }
  : HasQuery extends true
    ? { queryParams?: EmptyTypes }
    : QueryParams extends EmptyTypes | void | never
      ? { queryParams?: QueryParams }
      : {
          queryParams: QueryParams;
        };

export type RequestDynamicSendOptionsType<Request extends RequestInstance> = Omit<
  Partial<RequestOptionsType<string, ExtractAdapterOptionsType<ExtractAdapterType<Request>>>>,
  "params" | "data" | "endpoint" | "method"
> & {
  dispatcherType?: "auto" | "fetch" | "submit";
};

// Request making

export type RequestSendOptionsType<Request extends RequestInstance> = FetchQueryParamsType<
  ExtractQueryParamsType<Request>,
  ExtractHasQueryParamsType<Request>
> &
  FetchParamsType<ExtractParamsType<Request>, ExtractHasParamsType<Request>> &
  FetchPayloadType<ExtractPayloadType<Request>, ExtractHasPayloadType<Request>> &
  FetchQueryParamsType<ExtractQueryParamsType<Request>, ExtractHasQueryParamsType<Request>> &
  RequestSendActionsType<Request> &
  RequestDynamicSendOptionsType<Request>;

export type RequestSendActionsType<Request extends RequestInstance> = {
  onBeforeSent?: (eventData: RequestEventType<Request>) => void;
  onRequestStart?: (eventData: RequestEventType<Request>) => void;
  onResponseStart?: (eventData: RequestEventType<Request>) => void;
  onUploadProgress?: (eventData: RequestProgressEventType<Request>) => void;
  onDownloadProgress?: (eventData: RequestProgressEventType<Request>) => void;
  onResponse?: (eventData: RequestResponseEventType<Request>) => void;
  onRemove?: (eventData: RequestEventType<Request>) => void;
};

type IsNegativeType<T> = void extends T
  ? EmptyTypes
  : undefined extends T
    ? EmptyTypes
    : null extends T
      ? EmptyTypes
      : T;

// If no data or params provided - options should be optional. If either data or params are provided - mandatory.
export type RequestSendType<Request extends RequestInstance> =
  IsNegativeType<RequestSendOptionsType<Request>["payload"]> extends EmptyTypes | void | never
    ? IsNegativeType<RequestSendOptionsType<Request>["params"]> extends EmptyTypes | void | never
      ? IsNegativeType<RequestSendOptionsType<Request>["queryParams"]> extends EmptyTypes | void | never
        ? (options?: RequestSendOptionsType<Request>) => Promise<RequestResponseType<Request>>
        : (options: RequestSendOptionsType<Request>) => Promise<RequestResponseType<Request>>
      : (options: RequestSendOptionsType<Request>) => Promise<RequestResponseType<Request>>
    : (options: RequestSendOptionsType<Request>) => Promise<RequestResponseType<Request>>;

// Mappers

export type RequestMapper<Request extends RequestInstance, NewRequest extends RequestInstance> = (
  request: Request,
  requestId: string,
) => NewRequest | Promise<NewRequest>;

export type ResponseMapper<
  Request extends RequestInstance,
  MappedResponse extends ResponseSuccessType<any, any> | ResponseErrorType<any, any>,
> = (
  response:
    | ResponseSuccessType<ExtractResponseType<Request>, ExtractAdapterType<Request>>
    | ResponseErrorType<ExtractErrorType<Request>, ExtractAdapterType<Request>>,
) => MappedResponse | Promise<MappedResponse>;
